// Copyright 2016 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "netcp.h"

#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>

#include <inet6/inet6.h>
#include <inet6/netifc.h>

#include <zircon/processargs.h>
#include <zircon/syscalls.h>

#include <zircon/boot/netboot.h>

namespace {

constexpr char TMP_SUFFIX[] = ".netsvc.tmp";

struct netcp_state {
  int fd;
  off_t offset;
  // false: Filename is the open file and final destination
  // true : Filename is final destination; open file has a magic tmp suffix
  bool needs_rename;
  char filename[PATH_MAX];
};

netcp_state netcp = {
    .fd = -1,
    .offset = 0,
    .needs_rename = false,
    .filename = {0},
};

int netcp_mkdir(const char* filename) {
  const char* ptr = filename[0] == '/' ? filename + 1 : filename;
  struct stat st;
  char tmp[1024];
  for (;;) {
    ptr = strchr(ptr, '/');
    if (!ptr) {
      return 0;
    }
    memcpy(tmp, filename, ptr - filename);
    tmp[ptr - filename] = '\0';
    ptr += 1;
    if (stat(tmp, &st) < 0) {
      if (errno == ENOENT) {
        if (mkdir(tmp, 0755) < 0) {
          return -1;
        }
      } else {
        return -1;
      }
    }
  }
}

}  // namespace

int netcp_open(const char* filename, uint32_t arg, size_t* file_size) {
  if (netcp.fd >= 0) {
    printf("netsvc: closing still-open '%s', replacing with '%s'\n", netcp.filename, filename);
    close(netcp.fd);
    netcp.fd = -1;
  }
  size_t len = strlen(filename);
  strlcpy(netcp.filename, filename, sizeof(netcp.filename));

  struct stat st;
again:  // label here to catch filename=/path/to/new/directory/
  if (stat(filename, &st) == 0 && S_ISDIR(st.st_mode)) {
    errno = EISDIR;
    goto err;
  }

  switch (arg) {
    case O_RDONLY:
      netcp.needs_rename = false;
      netcp.fd = open(filename, O_RDONLY);
      if (file_size) {
        *file_size = st.st_size;
      }
      break;
    case O_WRONLY: {
      // If we're writing a file, actually write to "filename + TMP_SUFFIX",
      // and rename to the final destination when we would close. This makes
      // written files appear to atomically update.
      if (len + strlen(TMP_SUFFIX) + 1 > PATH_MAX) {
        errno = ENAMETOOLONG;
        goto err;
      }
      strcat(netcp.filename, TMP_SUFFIX);
      netcp.needs_rename = true;
      netcp.fd = open(netcp.filename, O_WRONLY | O_CREAT | O_TRUNC);
      netcp.filename[len] = '\0';
      if (netcp.fd < 0 && errno == ENOENT) {
        if (netcp_mkdir(filename) == 0) {
          goto again;
        }
      }
      break;
    }
    default:
      printf("netsvc: open '%s' with invalid mode %d\n", filename, arg);
      errno = EINVAL;
  }
  if (netcp.fd < 0) {
    goto err;
  } else {
    strlcpy(netcp.filename, filename, sizeof(netcp.filename));
    netcp.offset = 0;
  }

  return 0;
err:
  netcp.filename[0] = '\0';
  return -errno;
}

ssize_t netcp_offset_read(void* data_out, off_t offset, size_t max_len) {
  if (netcp.fd < 0) {
    printf("netsvc: read, but no open file\n");
    return -EBADF;
  }
  if (offset != netcp.offset) {
    if (lseek(netcp.fd, offset, SEEK_SET) != offset) {
      return -errno;
    }
    netcp.offset = offset;
  }
  return netcp_read(data_out, max_len);
}

ssize_t netcp_read(void* data_out, size_t data_sz) {
  if (netcp.fd < 0) {
    printf("netsvc: read, but no open file\n");
    return -EBADF;
  }
  ssize_t n = read(netcp.fd, data_out, data_sz);
  if (n < 0) {
    printf("netsvc: error reading '%s': %d\n", netcp.filename, errno);
    int result = (errno == 0) ? -EIO : -errno;
    close(netcp.fd);
    netcp.fd = -1;
    return result;
  }
  netcp.offset += n;
  return n;
}

ssize_t netcp_offset_write(const char* data, off_t offset, size_t length) {
  if (netcp.fd < 0) {
    printf("netsvc: write, but no open file\n");
    return -EBADF;
  }
  if (offset != netcp.offset) {
    if (lseek(netcp.fd, offset, SEEK_SET) != offset) {
      return -errno;
    }
    netcp.offset = offset;
  }
  return netcp_write(data, length);
}

ssize_t netcp_write(const char* data, size_t len) {
  if (netcp.fd < 0) {
    printf("netsvc: write, but no open file\n");
    return -EBADF;
  }
  ssize_t n = write(netcp.fd, data, len);
  if (n != static_cast<ssize_t>(len)) {
    printf("netsvc: error writing %s: %d\n", netcp.filename, errno);
    int result = (errno == 0) ? -EIO : -errno;
    close(netcp.fd);
    netcp.fd = -1;
    return result;
  }
  netcp.offset += len;
  return len;
}

int netcp_close() {
  int result = 0;
  if (netcp.fd < 0) {
    printf("netsvc: close, but no open file\n");
  } else {
    if (netcp.needs_rename) {
      char src[PATH_MAX];
      strlcpy(src, netcp.filename, sizeof(src));
      strlcat(src, TMP_SUFFIX, sizeof(src));
      if (rename(src, netcp.filename)) {
        printf("netsvc: failed to rename temporary file: %s\n", strerror(errno));
      }
    }
    if (close(netcp.fd)) {
      result = (errno == 0) ? -EIO : -errno;
    }
    netcp.fd = -1;
  }
  return result;
}

// Clean up if we abort before finishing a write. Close out and unlink it, rather than
// leaving an incomplete file.
void netcp_abort_write() {
  if (netcp.fd < 0) {
    return;
  }
  close(netcp.fd);
  netcp.fd = -1;
  char tmp[PATH_MAX];
  const char* filename;
  if (netcp.needs_rename) {
    strlcpy(tmp, netcp.filename, sizeof(tmp));
    strlcat(tmp, TMP_SUFFIX, sizeof(tmp));
    filename = tmp;
  } else {
    filename = netcp.filename;
  }
  if (unlink(filename) != 0) {
    printf("netsvc: failed to unlink aborted file %s\n", filename);
  }
}
